﻿// KeyboardSimulator.cs
using System.Runtime.InteropServices;
using System.Text;
using System.Diagnostics;

namespace SerialInput;

public abstract class KeyboardSimulator
{
#if WINDOWS
    #region Win32 API 声明
    [DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();
    [DllImport("user32.dll")]
    private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
    [DllImport("user32.dll")]
    private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
    [DllImport("user32.dll")]
    private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
    [DllImport("user32.dll")]
    private static extern bool OpenClipboard(IntPtr hWndNewOwner);
    [DllImport("user32.dll")]
    private static extern bool CloseClipboard();
    [DllImport("user32.dll")]
    private static extern bool EmptyClipboard();
    [DllImport("user32.dll")]
    private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
    [DllImport("user32.dll")]
    private static extern IntPtr GetClipboardData(uint uFormat);
    [DllImport("kernel32.dll")]
    private static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);
    [DllImport("kernel32.dll")]
    private static extern IntPtr GlobalLock(IntPtr hMem);
    [DllImport("kernel32.dll")]
    private static extern bool GlobalUnlock(IntPtr hMem);
    [DllImport("kernel32.dll")]
    private static extern IntPtr GlobalFree(IntPtr hMem);
    [DllImport("user32.dll")]
    private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
    [DllImport("user32.dll")]
    private static extern short VkKeyScan(char ch);
    [DllImport("user32.dll")]
    private static extern uint MapVirtualKey(uint uCode, uint uMapType);
    private const uint CF_UNICODETEXT = 13;
    private const uint GMEM_MOVEABLE = 0x0002;
    private const uint KEYEVENTF_KEYUP = 0x0002;
    private const uint KEYEVENTF_UNICODE = 0x0004;
    private const byte VK_CONTROL = 0x11;
    private const byte VK_V = 0x56;
    private const byte VK_RETURN = 0x0D;
    private const byte VK_TAB = 0x09;

    [StructLayout(LayoutKind.Sequential)]
    struct INPUT
    {
        public uint type;
        public InputUnion u;
    }

    [StructLayout(LayoutKind.Explicit)]
    struct InputUnion
    {
        [FieldOffset(0)] public KEYBDINPUT ki;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct KEYBDINPUT
    {
        public ushort wVk;
        public ushort wScan;
        public uint dwFlags;
        public uint time;
        public IntPtr dwExtraInfo;
    }

    const uint INPUT_KEYBOARD = 1;

    [DllImport("user32.dll", SetLastError = true)]
    static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
    #endregion
#endif

    public static FocusInfo GetFocusInfo()
    {
        var info = new FocusInfo();
#if WINDOWS
        try
        {
            IntPtr foregroundWindow = GetForegroundWindow();
            if (foregroundWindow == IntPtr.Zero)
                return info;
            StringBuilder windowTitle = new StringBuilder(256);
            GetWindowText(foregroundWindow, windowTitle, windowTitle.Capacity);
            info.WindowTitle = windowTitle.ToString();
            StringBuilder className = new StringBuilder(256);
            GetClassName(foregroundWindow, className, className.Capacity);
            info.WindowClassName = className.ToString();
            GetWindowThreadProcessId(foregroundWindow, out uint processId);
            info.ProcessId = processId;
            info.IsInputControl = IsInputControl(info.WindowClassName, info.WindowTitle);
        }
        catch { }
#else
        try
        {
            var psi = new ProcessStartInfo("xdotool", "getwindowfocus getwindowname")
            {
                RedirectStandardOutput = true
            };
            var proc = Process.Start(psi);
            string title = proc?.StandardOutput.ReadToEnd().Trim() ?? "";
            proc?.WaitForExit();
            info.WindowTitle = title;
            info.WindowClassName = "";
            info.ProcessId = 0;
            info.IsInputControl = !string.IsNullOrEmpty(title);
        }
        catch { }
#endif
        return info;
    }

    private static bool IsInputControl(string className, string windowTitle)
    {
        return true;
        string titleLower = windowTitle.ToLower();
        var inputTitles = new[] { "notepad", "记事本", "editor", "chrome", "firefox", "word", "excel", "terminal" };
        return inputTitles.Any(t => titleLower.Contains(t));
    }
    private static string GetClipboardTool()
    {
        var tools = new[] { "/usr/bin/xsel", "/usr/bin/xclip", "/bin/xsel", "/bin/xclip" };
        foreach (var tool in tools)
        {
            if (File.Exists(tool))
            {
                return tool;
            }
        }
        return null;
    }

    /// <summary>
    /// 剪切板粘贴方式发送文本
    /// </summary>
    public static bool SendTextClipboard(string text, bool addEnter = false, bool addTab = false)
    {
#if WINDOWS
        if (string.IsNullOrEmpty(text)) return false;
        bool success = SendTextViaClipboard(text);
        if (success)
        {
            if (addEnter) SendKey(VK_RETURN);
            if (addTab) SendKey(VK_TAB);
        }
        return success;
#else
        try
        {
            var clipTool = GetClipboardTool();
            if (clipTool == null) return false;
            string args = clipTool.Contains("xsel") ? "-b -i" : "-selection clipboard -i";
            var psi = new ProcessStartInfo(clipTool, args)
            {
                RedirectStandardInput = true,
                UseShellExecute = false
            };
            using (var proc = Process.Start(psi))
            {
                proc.StandardInput.Write(text);
                proc.StandardInput.Close();
                proc.WaitForExit();
            }
            Process.Start("/usr/bin/xdotool", "key --clearmodifiers ctrl+v")?.WaitForExit();
            return true;
        }
        catch { return false; }
#endif
    }

    /// <summary>
    /// 模拟逐字输入方式发送文本
    /// </summary>
    public static bool SendTextUnicode(string text, int delayMs = 10, bool addEnter = false, bool addTab = false)
    {
#if WINDOWS
        if (string.IsNullOrEmpty(text)) return false;
        foreach (char c in text)
        {
            SendTextViaUnicode(c.ToString());
            Thread.Sleep(delayMs);
        }
        if (addEnter) SendKey(VK_RETURN);
        if (addTab) SendKey(VK_TAB);
        return true;
#else
        try
        {
            foreach (char c in text)
            {
                Process.Start("/usr/bin/xdotool", $"key --clearmodifiers {c}")?.WaitForExit();
                Thread.Sleep(delayMs);
            }
            return true;
        }
        catch { return false; }
#endif
    }

#if WINDOWS
    private static bool SendTextViaClipboard(string text)
    {
        if (!SetClipboardText(text)) return false;
        keybd_event(VK_CONTROL, 0, 0, UIntPtr.Zero);
        keybd_event(VK_V, 0, 0, UIntPtr.Zero);
        keybd_event(VK_V, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
        keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
        return true;
    }
    private static bool SendTextViaUnicode(string text)
    {
        foreach (char c in text)
        {
            var inputs = new INPUT[]
            {
                new INPUT
                {
                    type = INPUT_KEYBOARD,
                    u = new InputUnion
                    {
                        ki = new KEYBDINPUT
                        {
                            wVk = 0,
                            wScan = c,
                            dwFlags = KEYEVENTF_UNICODE,
                            dwExtraInfo = IntPtr.Zero
                        }
                    }
                },
                new INPUT
                {
                    type = INPUT_KEYBOARD,
                    u = new InputUnion
                    {
                        ki = new KEYBDINPUT
                        {
                            wVk = 0,
                            wScan = c,
                            dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
                            dwExtraInfo = IntPtr.Zero
                        }
                    }
                }
            };
            SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(INPUT)));
        }
        return true;
    }
    private static bool SetClipboardText(string text)
    {
        if (!OpenClipboard(IntPtr.Zero)) return false;
        EmptyClipboard();
        byte[] bytes = Encoding.Unicode.GetBytes(text + '\0');
        IntPtr hGlobal = GlobalAlloc(GMEM_MOVEABLE, (UIntPtr)bytes.Length);
        IntPtr pGlobal = GlobalLock(hGlobal);
        Marshal.Copy(bytes, 0, pGlobal, bytes.Length);
        GlobalUnlock(hGlobal);
        SetClipboardData(CF_UNICODETEXT, hGlobal);
        CloseClipboard();
        return true;
    }
    private static void SendKey(byte vKey)
    {
        keybd_event(vKey, 0, 0, UIntPtr.Zero);
        keybd_event(vKey, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
    }
#endif
}

public class FocusInfo
{
    public string WindowTitle { get; set; } = "";
    public string WindowClassName { get; set; } = "";
    public uint ProcessId { get; set; }
    public bool IsInputControl { get; set; }
}